// Copyright Epic Games, Inc. All Rights Reserved.

#include <zencore/filesystem.h>

#include <zencore/compositebuffer.h>
#include <zencore/except.h>
#include <zencore/fmtutils.h>
#include <zencore/iobuffer.h>
#include <zencore/logging.h>
#include <zencore/process.h>
#include <zencore/stream.h>
#include <zencore/string.h>
#include <zencore/testing.h>

#if ZEN_PLATFORM_WINDOWS
#	include <zencore/windows.h>
#endif

#if ZEN_PLATFORM_WINDOWS
ZEN_THIRD_PARTY_INCLUDES_START
#	include <winioctl.h>
#	include <winnt.h>
ZEN_THIRD_PARTY_INCLUDES_END
#endif

#if ZEN_PLATFORM_LINUX
#	include <dirent.h>
#	include <fcntl.h>
#	include <sys/resource.h>
#	include <sys/stat.h>
#	include <unistd.h>
#endif

#if ZEN_PLATFORM_MAC
#	include <dirent.h>
#	include <fcntl.h>
#	include <libproc.h>
#	include <sys/resource.h>
#	include <sys/stat.h>
#	include <sys/syslimits.h>
#	include <unistd.h>
#endif

#include <fmt/format.h>
#include <filesystem>
#include <gsl/gsl-lite.hpp>

namespace zen {

using namespace std::literals;

#if ZEN_PLATFORM_WINDOWS

static bool
DeleteReparsePoint(const wchar_t* Path, DWORD dwReparseTag)
{
	windows::Handle hDir(CreateFileW(Path,
									 GENERIC_WRITE,
									 FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
									 nullptr,
									 OPEN_EXISTING,
									 FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
									 nullptr));

	if (hDir != INVALID_HANDLE_VALUE)
	{
		REPARSE_GUID_DATA_BUFFER Rgdb = {};
		Rgdb.ReparseTag				  = dwReparseTag;

		DWORD	   dwBytes;
		const BOOL bOK =
			DeviceIoControl(hDir, FSCTL_DELETE_REPARSE_POINT, &Rgdb, REPARSE_GUID_DATA_BUFFER_HEADER_SIZE, nullptr, 0, &dwBytes, nullptr);

		return bOK == TRUE;
	}

	return false;
}

bool
CreateDirectories(const wchar_t* Dir)
{
	// This may be suboptimal, in that it appears to try and create directories
	// from the root on up instead of from some directory which is known to
	// be present
	//
	// We should implement a smarter version at some point since this can be
	// pretty expensive in aggregate

	return std::filesystem::create_directories(Dir);
}

// Erase all files and directories in a given directory, leaving an empty directory
// behind

static bool
WipeDirectory(const wchar_t* DirPath, bool KeepDotFiles)
{
	ExtendableWideStringBuilder<128> Pattern;
	Pattern.Append(DirPath);
	Pattern.Append(L"\\*");

	WIN32_FIND_DATAW FindData;
	HANDLE			 hFind = FindFirstFileW(Pattern.c_str(), &FindData);

	bool Success = true;

	if (hFind != nullptr)
	{
		do
		{
			bool AttemptDelete = true;

			if (FindData.cFileName[0] == L'.')
			{
				if (FindData.cFileName[1] == L'.')
				{
					if (FindData.cFileName[2] == L'\0')
					{
						AttemptDelete = false;
					}
				}
				else if (FindData.cFileName[1] == L'\0')
				{
					AttemptDelete = false;
				}

				if (KeepDotFiles)
				{
					AttemptDelete = false;
				}
			}

			if (AttemptDelete)
			{
				ExtendableWideStringBuilder<128> Path;
				Path.Append(DirPath);
				Path.Append(L'\\');
				Path.Append(FindData.cFileName);

				// if (fd.dwFileAttributes & FILE_ATTRIBUTE_RECALL_ON_OPEN)
				//	deleteReparsePoint(path.c_str(), fd.dwReserved0);

				if (FindData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
				{
					if (FindData.dwFileAttributes & FILE_ATTRIBUTE_RECALL_ON_OPEN)
					{
						if (!DeleteReparsePoint(Path.c_str(), FindData.dwReserved0))
						{
							Success = false;
						}
					}

					if (FindData.dwFileAttributes & FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS)
					{
						if (!DeleteReparsePoint(Path.c_str(), FindData.dwReserved0))
						{
							Success = false;
						}
					}

					bool Succeeded = DeleteDirectories(Path.c_str());

					if (!Succeeded)
					{
						if (FindData.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)
						{
							if (!DeleteReparsePoint(Path.c_str(), FindData.dwReserved0))
							{
								Success = false;
							}
						}
					}
				}
				else
				{
					BOOL Succeeded = DeleteFileW(Path.c_str());

					if (!Succeeded)
					{
						// We should emit a warning here, but this is quite low level so care
						// needs to be taken.
						Success = false;
					}
				}
			}
		} while (FindNextFileW(hFind, &FindData) == TRUE);

		FindClose(hFind);
	}

	return true;
}

bool
DeleteDirectories(const wchar_t* DirPath)
{
	const bool KeepDotFiles = false;
	return WipeDirectory(DirPath, KeepDotFiles) && RemoveDirectoryW(DirPath) == TRUE;
}

bool
CleanDirectory(const wchar_t* DirPath)
{
	if (std::filesystem::exists(DirPath))
	{
		const bool KeepDotFiles = false;

		return WipeDirectory(DirPath, KeepDotFiles);
	}

	return CreateDirectories(DirPath);
}

bool
CleanDirectory(const wchar_t* DirPath, bool KeepDotFiles)
{
	if (std::filesystem::exists(DirPath))
	{
		return WipeDirectory(DirPath, KeepDotFiles);
	}

	return CreateDirectories(DirPath);
}

#endif	// ZEN_PLATFORM_WINDOWS

bool
CreateDirectories(const std::filesystem::path& Dir)
{
	if (Dir.string().ends_with(":"))
	{
		return false;
	}
	while (!std::filesystem::is_directory(Dir))
	{
		if (Dir.has_parent_path())
		{
			CreateDirectories(Dir.parent_path());
		}
		std::error_code ErrorCode;
		std::filesystem::create_directory(Dir, ErrorCode);
		if (ErrorCode)
		{
			throw std::system_error(ErrorCode, fmt::format("Failed to create directories for '{}'", Dir.string()));
		}
		return true;
	}
	return false;
}

bool
DeleteDirectories(const std::filesystem::path& Dir)
{
#if ZEN_PLATFORM_WINDOWS
	return DeleteDirectories(Dir.c_str());
#else
	std::error_code ErrorCode;
	return std::filesystem::remove_all(Dir, ErrorCode);
#endif
}

bool
CleanDirectory(const std::filesystem::path& Dir)
{
#if ZEN_PLATFORM_WINDOWS
	return CleanDirectory(Dir.c_str());
#else
	if (std::filesystem::exists(Dir))
	{
		bool Success = true;

		std::error_code ErrorCode;
		for (const auto& Item : std::filesystem::directory_iterator(Dir))
		{
			Success &= std::filesystem::remove_all(Item, ErrorCode);
		}

		return Success;
	}

	return CreateDirectories(Dir);
#endif
}

bool
CleanDirectoryExceptDotFiles(const std::filesystem::path& Dir)
{
#if ZEN_PLATFORM_WINDOWS
	const bool KeepDotFiles = true;
	return CleanDirectory(Dir.c_str(), KeepDotFiles);
#else
	ZEN_UNUSED(Dir);

	ZEN_NOT_IMPLEMENTED();
#endif
}

//////////////////////////////////////////////////////////////////////////

bool
SupportsBlockRefCounting(std::filesystem::path Path)
{
#if ZEN_PLATFORM_WINDOWS
	windows::Handle Handle(CreateFileW(Path.c_str(),
									   GENERIC_READ,
									   FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
									   nullptr,
									   OPEN_EXISTING,
									   FILE_FLAG_BACKUP_SEMANTICS,
									   nullptr));

	if (Handle == INVALID_HANDLE_VALUE)
	{
		Handle.Detach();
		return false;
	}

	ULONG FileSystemFlags = 0;
	if (!GetVolumeInformationByHandleW(Handle, nullptr, 0, nullptr, nullptr, /* lpFileSystemFlags */ &FileSystemFlags, nullptr, 0))
	{
		return false;
	}

	if (!(FileSystemFlags & FILE_SUPPORTS_BLOCK_REFCOUNTING))
	{
		return false;
	}

	return true;
#else
	ZEN_UNUSED(Path);
	return false;
#endif	// ZEN_PLATFORM_WINDOWS
}

static bool
CloneFile(std::filesystem::path FromPath, std::filesystem::path ToPath)
{
#if ZEN_PLATFORM_WINDOWS
	windows::Handle FromFile(CreateFileW(FromPath.c_str(),
										 GENERIC_READ,
										 FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
										 nullptr,
										 OPEN_EXISTING,
										 0,
										 nullptr));
	if (FromFile == INVALID_HANDLE_VALUE)
	{
		FromFile.Detach();
		return false;
	}

	ULONG FileSystemFlags;
	if (!GetVolumeInformationByHandleW(FromFile, nullptr, 0, nullptr, nullptr, /* lpFileSystemFlags */ &FileSystemFlags, nullptr, 0))
	{
		return false;
	}
	if (!(FileSystemFlags & FILE_SUPPORTS_BLOCK_REFCOUNTING))
	{
		SetLastError(ERROR_NOT_CAPABLE);
		return false;
	}

	FILE_END_OF_FILE_INFO FileSize;
	if (!GetFileSizeEx(FromFile, &FileSize.EndOfFile))
	{
		return false;
	}

	FILE_BASIC_INFO BasicInfo;
	if (!GetFileInformationByHandleEx(FromFile, FileBasicInfo, &BasicInfo, sizeof BasicInfo))
	{
		return false;
	}

	DWORD								   dwBytesReturned = 0;
	FSCTL_GET_INTEGRITY_INFORMATION_BUFFER GetIntegrityInfoBuffer;
	if (!DeviceIoControl(FromFile,
						 FSCTL_GET_INTEGRITY_INFORMATION,
						 nullptr,
						 0,
						 &GetIntegrityInfoBuffer,
						 sizeof GetIntegrityInfoBuffer,
						 &dwBytesReturned,
						 nullptr))
	{
		return false;
	}

	SetFileAttributesW(ToPath.c_str(), FILE_ATTRIBUTE_NORMAL);

	windows::Handle TargetFile(CreateFileW(ToPath.c_str(),
										   GENERIC_READ | GENERIC_WRITE | DELETE,
										   /* no sharing */ FILE_SHARE_READ,
										   nullptr,
										   OPEN_ALWAYS,
										   0,
										   /* hTemplateFile */ FromFile));

	if (TargetFile == INVALID_HANDLE_VALUE)
	{
		TargetFile.Detach();
		return false;
	}

	// Delete target file when handle is closed (we only reset this if the copy succeeds)
	FILE_DISPOSITION_INFO FileDisposition = {TRUE};
	if (!SetFileInformationByHandle(TargetFile, FileDispositionInfo, &FileDisposition, sizeof FileDisposition))
	{
		const DWORD ErrorCode = ::GetLastError();
		TargetFile.Close();
		DeleteFileW(ToPath.c_str());
		SetLastError(ErrorCode);
		return false;
	}

	// Make file sparse so we don't end up allocating space when we change the file size
	if (!DeviceIoControl(TargetFile, FSCTL_SET_SPARSE, nullptr, 0, nullptr, 0, &dwBytesReturned, nullptr))
	{
		return false;
	}

	// Copy integrity checking information
	FSCTL_SET_INTEGRITY_INFORMATION_BUFFER IntegritySet = {GetIntegrityInfoBuffer.ChecksumAlgorithm,
														   GetIntegrityInfoBuffer.Reserved,
														   GetIntegrityInfoBuffer.Flags};
	if (!DeviceIoControl(TargetFile, FSCTL_SET_INTEGRITY_INFORMATION, &IntegritySet, sizeof IntegritySet, nullptr, 0, nullptr, nullptr))
	{
		return false;
	}

	// Resize file - note that the file is sparse at this point so no additional data will be written
	if (!SetFileInformationByHandle(TargetFile, FileEndOfFileInfo, &FileSize, sizeof FileSize))
	{
		return false;
	}

	constexpr auto RoundToClusterSize = [](LONG64 FileSize, ULONG ClusterSize) -> LONG64 {
		return (FileSize + ClusterSize - 1) / ClusterSize * ClusterSize;
	};
	static_assert(RoundToClusterSize(5678, 4 * 1024) == 8 * 1024);

	// Loop for cloning file contents. This is necessary as the API has a 32-bit size
	// limit for some reason

	const LONG64 SplitThreshold = (1LL << 32) - GetIntegrityInfoBuffer.ClusterSizeInBytes;

	DUPLICATE_EXTENTS_DATA DuplicateExtentsData{.FileHandle = FromFile};

	for (LONG64 CurrentByteOffset = 0,
				RemainingBytes	  = RoundToClusterSize(FileSize.EndOfFile.QuadPart, GetIntegrityInfoBuffer.ClusterSizeInBytes);
		 RemainingBytes > 0;
		 CurrentByteOffset += SplitThreshold, RemainingBytes -= SplitThreshold)
	{
		DuplicateExtentsData.SourceFileOffset.QuadPart = CurrentByteOffset;
		DuplicateExtentsData.TargetFileOffset.QuadPart = CurrentByteOffset;
		DuplicateExtentsData.ByteCount.QuadPart		   = std::min(SplitThreshold, RemainingBytes);

		if (!DeviceIoControl(TargetFile,
							 FSCTL_DUPLICATE_EXTENTS_TO_FILE,
							 &DuplicateExtentsData,
							 sizeof DuplicateExtentsData,
							 nullptr,
							 0,
							 &dwBytesReturned,
							 nullptr))
		{
			return false;
		}
	}

	// Make the file not sparse again now that we have populated the contents
	if (!(BasicInfo.FileAttributes & FILE_ATTRIBUTE_SPARSE_FILE))
	{
		FILE_SET_SPARSE_BUFFER SetSparse = {FALSE};

		if (!DeviceIoControl(TargetFile, FSCTL_SET_SPARSE, &SetSparse, sizeof SetSparse, nullptr, 0, &dwBytesReturned, nullptr))
		{
			return false;
		}
	}

	// Update timestamps (but don't lie about the creation time)
	BasicInfo.CreationTime.QuadPart = 0;
	if (!SetFileInformationByHandle(TargetFile, FileBasicInfo, &BasicInfo, sizeof BasicInfo))
	{
		return false;
	}

	if (!FlushFileBuffers(TargetFile))
	{
		return false;
	}

	// Finally now everything is done - make sure the file is not deleted on close!

	FileDisposition = {FALSE};

	const bool AllOk = (TRUE == SetFileInformationByHandle(TargetFile, FileDispositionInfo, &FileDisposition, sizeof FileDisposition));

	return AllOk;
#elif ZEN_PLATFORM_LINUX
#	if 0
	struct ScopedFd
	{
		~ScopedFd() { close(Fd); }
		int Fd;
	};

	// The 'from' file
	int FromFd = open(FromPath.c_str(), O_RDONLY|O_CLOEXEC);
	if (FromFd < 0)
	{
		return false;
	}
	ScopedFd $From = { FromFd };

	// The 'to' file
	int ToFd = open(ToPath.c_str(), O_WRONLY|O_CREAT|O_EXCL|O_CLOEXEC, 0666);
	if (ToFd < 0)
	{
		return false;
	}
	fchmod(ToFd, 0666);
	ScopedFd $To = { FromFd };

	ioctl(ToFd, FICLONE, FromFd);

	return false;
#	endif	// 0
	ZEN_UNUSED(FromPath, ToPath);
	ZEN_ERROR("CloneFile() is not implemented on this platform");
	return false;
#elif ZEN_PLATFORM_MAC
	/* clonefile() syscall if APFS */
	ZEN_UNUSED(FromPath, ToPath);
	ZEN_ERROR("CloneFile() is not implemented on this platform");
	return false;
#endif	// ZEN_PLATFORM_WINDOWS
}

void
CopyFile(std::filesystem::path FromPath, std::filesystem::path ToPath, const CopyFileOptions& Options, std::error_code& OutErrorCode)
{
	OutErrorCode.clear();

	bool Success = CopyFile(FromPath, ToPath, Options);

	if (!Success)
	{
		OutErrorCode = MakeErrorCodeFromLastError();
	}
}

bool
CopyFile(std::filesystem::path FromPath, std::filesystem::path ToPath, const CopyFileOptions& Options)
{
	bool Success = false;

	if (Options.EnableClone)
	{
		Success = CloneFile(FromPath.native(), ToPath.native());

		if (Success)
		{
			return true;
		}
	}

	if (Options.MustClone)
	{
		return false;
	}

#if ZEN_PLATFORM_WINDOWS
	BOOL CancelFlag = FALSE;
	Success			= !!::CopyFileExW(FromPath.c_str(),
							  ToPath.c_str(),
							  /* lpProgressRoutine */ nullptr,
							  /* lpData */ nullptr,
							  &CancelFlag,
							  /* dwCopyFlags */ 0);
#else
	struct ScopedFd
	{
		~ScopedFd() { close(Fd); }
		int Fd;
	};

	// From file
	int FromFd = open(FromPath.c_str(), O_RDONLY | O_CLOEXEC);
	if (FromFd < 0)
	{
		ThrowLastError(fmt::format("failed to open file {}", FromPath));
	}
	ScopedFd $From = {FromFd};

	// To file
	int ToFd = open(ToPath.c_str(), O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0666);
	if (ToFd < 0)
	{
		ThrowLastError(fmt::format("failed to create file {}", ToPath));
	}
	fchmod(ToFd, 0666);
	ScopedFd $To = {ToFd};

	// Copy impl
	static const size_t BufferSize = 64 << 10;
	void*				Buffer	   = malloc(BufferSize);
	while (true)
	{
		int BytesRead = read(FromFd, Buffer, BufferSize);
		if (BytesRead <= 0)
		{
			Success = (BytesRead == 0);
			break;
		}

		if (write(ToFd, Buffer, BytesRead) != BufferSize)
		{
			Success = false;
			break;
		}
	}
	free(Buffer);
#endif	// ZEN_PLATFORM_WINDOWS

	if (!Success)
	{
		ThrowLastError("file copy failed"sv);
	}

	return true;
}

void
CopyTree(std::filesystem::path FromPath, std::filesystem::path ToPath, const CopyFileOptions& Options)
{
	// Validate arguments

	if (FromPath.empty() || !std::filesystem::is_directory(FromPath))
		throw std::runtime_error("invalid CopyTree source directory specified");

	if (ToPath.empty())
		throw std::runtime_error("no CopyTree target specified");

	if (Options.MustClone && !SupportsBlockRefCounting(FromPath))
		throw std::runtime_error(fmt::format("cloning not possible from '{}'", FromPath));

	if (std::filesystem::exists(ToPath))
	{
		if (!std::filesystem::is_directory(ToPath))
		{
			throw std::runtime_error(fmt::format("specified CopyTree target '{}' is not a directory", ToPath));
		}
	}
	else
	{
		std::filesystem::create_directories(ToPath);
	}

	if (Options.MustClone && !SupportsBlockRefCounting(ToPath))
		throw std::runtime_error(fmt::format("cloning not possible from '{}'", ToPath));

	// Verify source/target relationships

	std::error_code		  Ec;
	std::filesystem::path FromCanonical = std::filesystem::canonical(FromPath, Ec);

	if (!Ec)
	{
		std::filesystem::path ToCanonical = std::filesystem::canonical(ToPath, Ec);

		if (!Ec)
		{
			if (FromCanonical == ToCanonical)
			{
				throw std::runtime_error("Target and source must be distinct files or directories");
			}

			if (ToCanonical.generic_string().starts_with(FromCanonical.generic_string()) ||
				FromCanonical.generic_string().starts_with(ToCanonical.generic_string()))
			{
				throw std::runtime_error("Invalid parent/child relationship for source/target directories");
			}
		}
	}

	struct CopyVisitor : public FileSystemTraversal::TreeVisitor
	{
		CopyVisitor(std::filesystem::path InBasePath, zen::CopyFileOptions InCopyOptions) : BasePath(InBasePath), CopyOptions(InCopyOptions)
		{
		}

		virtual void VisitFile(const std::filesystem::path& Parent, const path_view& File, uint64_t FileSize) override
		{
			std::error_code				Ec;
			const std::filesystem::path Relative = std::filesystem::relative(Parent, BasePath, Ec);

			if (Ec)
			{
				FailedFileCount++;
			}
			else
			{
				const std::filesystem::path FromPath = Parent / File;
				std::filesystem::path		ToPath;

				if (Relative.compare("."))
				{
					zen::CreateDirectories(TargetPath / Relative);

					ToPath = TargetPath / Relative / File;
				}
				else
				{
					ToPath = TargetPath / File;
				}

				try
				{
					if (zen::CopyFile(FromPath, ToPath, CopyOptions))
					{
						++FileCount;
						ByteCount += FileSize;
					}
					else
					{
						throw std::runtime_error("CopyFile failed in an unexpected way");
					}
				}
				catch (const std::exception& Ex)
				{
					++FailedFileCount;

					throw std::runtime_error(fmt::format("failed to copy '{}' to '{}': '{}'", FromPath, ToPath, Ex.what()));
				}
			}
		}

		virtual bool VisitDirectory(const std::filesystem::path&, const path_view&) override { return true; }

		std::filesystem::path BasePath;
		std::filesystem::path TargetPath;
		zen::CopyFileOptions  CopyOptions;
		int					  FileCount		  = 0;
		uint64_t			  ByteCount		  = 0;
		int					  FailedFileCount = 0;
	};

	CopyVisitor Visitor{FromPath, Options};
	Visitor.TargetPath = ToPath;

	FileSystemTraversal Traversal;
	Traversal.TraverseFileSystem(FromPath, Visitor);

	if (Visitor.FailedFileCount)
	{
		throw std::runtime_error(fmt::format("{} file copy operations FAILED", Visitor.FailedFileCount));
	}
}

void
WriteFile(std::filesystem::path Path, const IoBuffer* const* Data, size_t BufferCount)
{
#if ZEN_PLATFORM_WINDOWS
	windows::FileHandle Outfile;
	HRESULT				hRes = Outfile.Create(Path.c_str(), GENERIC_WRITE, FILE_SHARE_READ, CREATE_ALWAYS);
	if (hRes == HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND))
	{
		CreateDirectories(Path.parent_path());

		hRes = Outfile.Create(Path.c_str(), GENERIC_WRITE, FILE_SHARE_READ, CREATE_ALWAYS);
	}

	if (FAILED(hRes))
	{
		ThrowSystemException(hRes, fmt::format("File open failed for '{}'", Path).c_str());
	}

#else
	int OpenFlags = O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC;
	int Fd		  = open(Path.c_str(), OpenFlags, 0666);
	if (Fd < 0)
	{
		zen::CreateDirectories(Path.parent_path());
		Fd = open(Path.c_str(), OpenFlags, 0666);
	}

	if (Fd < 0)
	{
		ThrowLastError(fmt::format("File open failed for '{}'", Path));
	}

	fchmod(Fd, 0666);
#endif

	// TODO: this should be block-enlightened

	for (size_t i = 0; i < BufferCount; ++i)
	{
		uint64_t	WriteSize = Data[i]->Size();
		const void* DataPtr	  = Data[i]->Data();

		while (WriteSize)
		{
			const uint64_t ChunkSize = Min<uint64_t>(WriteSize, uint64_t(2) * 1024 * 1024 * 1024);

#if ZEN_PLATFORM_WINDOWS
			hRes = Outfile.Write(DataPtr, gsl::narrow_cast<uint32_t>(WriteSize));
			if (FAILED(hRes))
			{
				Outfile.Close();
				std::error_code DummyEc;
				std::filesystem::remove(Path, DummyEc);
				ThrowSystemException(hRes, fmt::format("File write failed for '{}'", Path).c_str());
			}
#else
			if (write(Fd, DataPtr, WriteSize) != int64_t(WriteSize))
			{
				close(Fd);
				std::error_code DummyEc;
				std::filesystem::remove(Path, DummyEc);
				ThrowLastError(fmt::format("File write failed for '{}'", Path));
			}
#endif	// ZEN_PLATFORM_WINDOWS

			WriteSize -= ChunkSize;
			DataPtr = reinterpret_cast<const uint8_t*>(DataPtr) + ChunkSize;
		}
	}

#if ZEN_PLATFORM_WINDOWS
	Outfile.Close();
#else
	close(Fd);
#endif
}

void
WriteFile(std::filesystem::path Path, IoBuffer Data)
{
	const IoBuffer* const DataPtr = &Data;

	WriteFile(Path, &DataPtr, 1);
}

void
WriteFile(std::filesystem::path Path, CompositeBuffer InData)
{
	std::vector<IoBuffer> DataVec;

	for (const SharedBuffer& Segment : InData.GetSegments())
	{
		DataVec.push_back(Segment.AsIoBuffer());
	}

	std::vector<const IoBuffer*> DataPtrs;

	for (IoBuffer& Data : DataVec)
	{
		DataPtrs.push_back(&Data);
	}

	WriteFile(Path, DataPtrs.data(), DataPtrs.size());
}

bool
MoveToFile(std::filesystem::path Path, IoBuffer Data)
{
	if (!Data.IsWholeFile())
	{
		return false;
	}
	IoBufferFileReference FileRef;
	if (!Data.GetFileReference(/* out */ FileRef))
	{
		return false;
	}

#if ZEN_PLATFORM_WINDOWS
	const HANDLE	  ChunkFileHandle = FileRef.FileHandle;
	std::wstring	  FileName		  = Path.native();
	const DWORD		  BufferSize	  = sizeof(FILE_RENAME_INFO) + gsl::narrow<DWORD>(FileName.size() * sizeof(WCHAR));
	FILE_RENAME_INFO* RenameInfo	  = reinterpret_cast<FILE_RENAME_INFO*>(Memory::Alloc(BufferSize));
	memset(RenameInfo, 0, BufferSize);

	RenameInfo->ReplaceIfExists = TRUE;
	RenameInfo->FileNameLength	= gsl::narrow<DWORD>(FileName.size());
	memcpy(RenameInfo->FileName, FileName.c_str(), FileName.size() * sizeof(WCHAR));
	RenameInfo->FileName[FileName.size()] = 0;

	// Try to move file into place
	BOOL Success = SetFileInformationByHandle(ChunkFileHandle, FileRenameInfo, RenameInfo, BufferSize);

	if (!Success)
	{
		DWORD LastError = GetLastError();
		if (LastError == ERROR_PATH_NOT_FOUND)
		{
			zen::CreateDirectories(Path.parent_path());
			Success = SetFileInformationByHandle(ChunkFileHandle, FileRenameInfo, RenameInfo, BufferSize);
		}
	}
	Memory::Free(RenameInfo);
	if (!Success)
	{
		return false;
	}
#elif ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC
	std::filesystem::path SourcePath = PathFromHandle(FileRef.FileHandle);
	int					  Ret		 = link(SourcePath.c_str(), Path.c_str());
	if (Ret < 0)
	{
		int32_t err = errno;
		if (err == ENOENT)
		{
			zen::CreateDirectories(Path.parent_path());
			Ret = link(SourcePath.c_str(), Path.c_str());
		}
	}
	if (Ret < 0)
	{
		return false;
	}
#endif	// ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC
	Data.SetDeleteOnClose(false);
	return true;
}

IoBuffer
FileContents::Flatten()
{
	if (Data.size() == 1)
	{
		return Data[0];
	}
	else if (Data.empty())
	{
		return {};
	}
	else
	{
		ZEN_NOT_IMPLEMENTED();
	}
}

FileContents
ReadStdIn()
{
	BinaryWriter Writer;

	do
	{
		uint8_t ReadBuffer[1024];

		size_t BytesRead = fread(ReadBuffer, 1, sizeof ReadBuffer, stdin);
		Writer.Write(ReadBuffer, BytesRead);
	} while (!feof(stdin));

	FileContents Contents;
	Contents.Data.emplace_back(IoBuffer(IoBuffer::Clone, Writer.GetData(), Writer.GetSize()));

	return Contents;
}

FileContents
ReadFile(std::filesystem::path Path)
{
	uint64_t FileSizeBytes;
	void*	 Handle;

#if ZEN_PLATFORM_WINDOWS
	windows::Handle FromFile(CreateFileW(Path.c_str(),
										 GENERIC_READ,
										 FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
										 nullptr,
										 OPEN_EXISTING,
										 0,
										 nullptr));
	if (FromFile == INVALID_HANDLE_VALUE)
	{
		FromFile.Detach();
		return FileContents{.ErrorCode = std::error_code(::GetLastError(), std::system_category())};
	}

	FILE_END_OF_FILE_INFO FileSize;
	if (!GetFileSizeEx(FromFile, &FileSize.EndOfFile))
	{
		return FileContents{.ErrorCode = std::error_code(::GetLastError(), std::system_category())};
	}

	FileSizeBytes = FileSize.EndOfFile.QuadPart;
	Handle		  = FromFile.Detach();
#else
	int Fd = open(Path.c_str(), O_RDONLY | O_CLOEXEC);
	if (Fd < 0)
	{
		FileContents Ret;
		Ret.ErrorCode = std::error_code(zen::GetLastError(), std::system_category());
		return Ret;
	}

	static_assert(sizeof(decltype(stat::st_size)) == sizeof(uint64_t), "fstat() doesn't support large files");
	struct stat Stat;
	fstat(Fd, &Stat);

	FileSizeBytes = Stat.st_size;
	Handle		  = (void*)uintptr_t(Fd);
#endif

	FileContents Contents;
	Contents.Data.emplace_back(IoBuffer(IoBuffer::File, Handle, 0, FileSizeBytes, /*IsWholeFile*/ true));
	return Contents;
}

ZENCORE_API void
ScanFile(void*												  NativeHandle,
		 uint64_t											  Offset,
		 uint64_t											  Size,
		 uint64_t											  ChunkSize,
		 std::function<void(const void* Data, size_t Size)>&& ProcessFunc)
{
	ZEN_ASSERT(NativeHandle != nullptr);
	uint64_t			 BufferSize = Min(ChunkSize, Size);
	std::vector<uint8_t> ReadBuffer(BufferSize);
	uint64_t			 ReadOffset = 0;
	while (ReadOffset < Size)
	{
		const uint64_t NumberOfBytesToRead = Min(Size - ReadOffset, BufferSize);
		uint64_t	   FileOffset		   = Offset + ReadOffset;

#if ZEN_PLATFORM_WINDOWS
		OVERLAPPED Ovl{};

		Ovl.Offset	   = DWORD(FileOffset & 0xffff'ffffu);
		Ovl.OffsetHigh = DWORD(FileOffset >> 32);

		DWORD BytesRead = 0;
		BOOL  Success	= ::ReadFile(NativeHandle, ReadBuffer.data(), DWORD(NumberOfBytesToRead), &BytesRead, &Ovl);
		if (!Success)
		{
			throw std::system_error(std::error_code(::GetLastError(), std::system_category()), "file scan failed");
		}
#else
		   int BytesRead = pread(int(intptr_t(NativeHandle)), ReadBuffer.data(), size_t(NumberOfBytesToRead), off_t(FileOffset));
		   if (BytesRead < 0)
		   {
			   throw std::system_error(std::error_code(errno, std::system_category()), "file scan failed");
		   }
#endif
		ProcessFunc(ReadBuffer.data(), (size_t)BytesRead);
		ReadOffset += (uint64_t)BytesRead;
	}
}

bool
ScanFile(std::filesystem::path Path, const uint64_t ChunkSize, std::function<void(const void* Data, size_t Size)>&& ProcessFunc)
{
#if ZEN_PLATFORM_WINDOWS
	windows::Handle FromFile(CreateFileW(Path.c_str(),
										 GENERIC_READ,
										 FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
										 nullptr,
										 OPEN_EXISTING,
										 0,
										 nullptr));
	if (FromFile == INVALID_HANDLE_VALUE)
	{
		FromFile.Detach();
		return false;
	}

	std::vector<uint8_t> ReadBuffer(ChunkSize);

	for (;;)
	{
		DWORD dwBytesRead = 0;
		BOOL  Success	  = ::ReadFile(FromFile, ReadBuffer.data(), (DWORD)ReadBuffer.size(), &dwBytesRead, nullptr);

		if (!Success)
		{
			throw std::system_error(std::error_code(::GetLastError(), std::system_category()), "file scan failed");
		}

		if (dwBytesRead == 0)
			break;

		ProcessFunc(ReadBuffer.data(), dwBytesRead);
	}
#else
	int Fd = open(Path.c_str(), O_RDONLY | O_CLOEXEC);
	if (Fd < 0)
	{
		return false;
	}

	bool Success = true;

	void* Buffer = malloc(ChunkSize);
	while (true)
	{
		int BytesRead = read(Fd, Buffer, ChunkSize);
		if (BytesRead < 0)
		{
			Success = false;
			break;
		}

		if (BytesRead == 0)
		{
			break;
		}

		ProcessFunc(Buffer, BytesRead);
	}

	free(Buffer);
	close(Fd);

	if (!Success)
	{
		ThrowLastError("file scan failed");
	}
#endif	// ZEN_PLATFORM_WINDOWS

	return true;
}

void
PathToUtf8(const std::filesystem::path& Path, StringBuilderBase& Out)
{
#if ZEN_PLATFORM_WINDOWS
	WideToUtf8(Path.native().c_str(), Out);
#else
	Out << Path.c_str();
#endif
}

std::string
PathToUtf8(const std::filesystem::path& Path)
{
#if ZEN_PLATFORM_WINDOWS
	return WideToUtf8(Path.native().c_str());
#else
	return Path.string();
#endif
}

DiskSpace
DiskSpaceInfo(std::filesystem::path Directory, std::error_code& Error)
{
	using namespace std::filesystem;

	space_info SpaceInfo = space(Directory, Error);
	if (Error)
	{
		return {};
	}

	return {
		.Free  = uint64_t(SpaceInfo.available),
		.Total = uint64_t(SpaceInfo.capacity),
	};
}

void
FileSystemTraversal::TraverseFileSystem(const std::filesystem::path& RootDir, TreeVisitor& Visitor)
{
#if ZEN_PLATFORM_WINDOWS
	uint64_t FileInfoBuffer[8 * 1024];

	FILE_INFO_BY_HANDLE_CLASS FibClass = FileIdBothDirectoryRestartInfo;
	bool					  Continue = true;

	windows::FileHandle RootDirHandle;
	HRESULT				hRes =
		RootDirHandle.Create(RootDir.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS);

	if (FAILED(hRes))
	{
		if (hRes == ERROR_FILE_NOT_FOUND)
		{
			// Directory no longer exist, treat it as empty
			return;
		}
		ThrowSystemException(hRes, fmt::format("Failed to open handle to '{}'", RootDir));
	}

	while (Continue)
	{
		BOOL Success = GetFileInformationByHandleEx(RootDirHandle, FibClass, FileInfoBuffer, sizeof FileInfoBuffer);
		FibClass	 = FileIdBothDirectoryInfo;	 // Set up for next iteration

		uint64_t EntryOffset = 0;

		if (!Success)
		{
			DWORD LastError = GetLastError();

			if (LastError == ERROR_NO_MORE_FILES)
			{
				break;
			}

			throw std::system_error(std::error_code(LastError, std::system_category()), "file system traversal error");
		}

		for (;;)
		{
			const FILE_ID_BOTH_DIR_INFO* DirInfo =
				reinterpret_cast<const FILE_ID_BOTH_DIR_INFO*>(reinterpret_cast<const uint8_t*>(FileInfoBuffer) + EntryOffset);

			std::wstring_view FileName(DirInfo->FileName, DirInfo->FileNameLength / sizeof(wchar_t));

			if (DirInfo->FileAttributes & FILE_ATTRIBUTE_DIRECTORY)
			{
				if (FileName == L"."sv || FileName == L".."sv)
				{
					// Not very interesting
				}
				else
				{
					const bool ShouldDescend = Visitor.VisitDirectory(RootDir, FileName);

					if (ShouldDescend)
					{
						// Note that this recursion combined with the buffer could
						// blow the stack, we should consider a different strategy

						std::filesystem::path FullPath = RootDir / FileName;

						TraverseFileSystem(FullPath, Visitor);
					}
				}
			}
			else if (DirInfo->FileAttributes & FILE_ATTRIBUTE_DEVICE)
			{
				ZEN_WARN("encountered device node during file system traversal: '{}' found in '{}'", WideToUtf8(FileName), RootDir);
			}
			else
			{
				Visitor.VisitFile(RootDir, FileName, DirInfo->EndOfFile.QuadPart);
			}

			const uint64_t NextOffset = DirInfo->NextEntryOffset;

			if (NextOffset == 0)
			{
				break;
			}

			EntryOffset += NextOffset;
		}
	}
#else
	/* Could also implement this using Linux's getdents() syscall */

	DIR* Dir = opendir(RootDir.c_str());
	if (Dir == nullptr)
	{
		int Err = errno;
		if (Err == ENOENT)
		{
			// Directory no longer exist, treat it as empty
			return;
		}
		ThrowLastError(fmt::format("Failed to open directory for traversal: {}", RootDir.c_str()));
	}

	for (struct dirent* Entry; (Entry = readdir(Dir));)
	{
		const char* FileName = Entry->d_name;

		struct stat			  Stat;
		std::filesystem::path FullPath = RootDir / FileName;
		stat(FullPath.c_str(), &Stat);

		if (S_ISDIR(Stat.st_mode))
		{
			if (strcmp(FileName, ".") == 0 || strcmp(FileName, "..") == 0)
			{
				/* nop */
			}
			else if (Visitor.VisitDirectory(RootDir, FileName))
			{
				TraverseFileSystem(FullPath, Visitor);
			}
		}
		else if (S_ISREG(Stat.st_mode))
		{
			Visitor.VisitFile(RootDir, FileName, Stat.st_size);
		}
		else
		{
			ZEN_WARN("encountered non-regular file during file system traversal ({}): {} found in {}",
					 Stat.st_mode,
					 FileName,
					 RootDir.c_str());
		}
	}

	closedir(Dir);
#endif	// ZEN_PLATFORM_WINDOWS
}

std::filesystem::path
CanonicalPath(std::filesystem::path InPath, std::error_code& Ec)
{
	ZEN_UNUSED(Ec);

#if ZEN_PLATFORM_WINDOWS
	windows::FileHandle Handle;
	HRESULT				hRes = Handle.Create(InPath.c_str(),
								 GENERIC_READ,
								 FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
								 OPEN_EXISTING,
								 FILE_FLAG_BACKUP_SEMANTICS);

	if (FAILED(hRes))
	{
		Ec = MakeErrorCodeFromLastError();

		return {};
	}

	return PathFromHandle(Handle, Ec);
#else
	return InPath;
#endif
}

std::filesystem::path
PathFromHandle(void* NativeHandle, std::error_code& Ec)
{
	if (NativeHandle == nullptr)
	{
		return "<error handle 'nullptr'>";
	}
#if ZEN_PLATFORM_WINDOWS
	if (NativeHandle == INVALID_HANDLE_VALUE)
	{
		return "<error handle 'invalid handle'>";
	}

	auto GetFinalPathNameByHandleWRetry =
		[&Ec](HANDLE hFile, LPWSTR lpszFilePath, DWORD cchFilePath, DWORD dwFlags, DWORD& OutRequiredLength) -> DWORD {
		size_t MaxTries = 5;
		while (MaxTries > 0)
		{
			MaxTries--;
			DWORD Res = GetFinalPathNameByHandleW(hFile, lpszFilePath, cchFilePath, dwFlags);
			if (Res == 0)
			{
				DWORD LastError = zen::GetLastError();
				// Under heavy concurrent loads we might get access denied on a file handle while trying to get path name.
				// Retry if that is the case.
				if (LastError != ERROR_ACCESS_DENIED)
				{
					Sleep(2);
					return LastError;
				}
				// Retry
				continue;
			}
			ZEN_ASSERT(Res != 1);  // We don't accept empty path names
			OutRequiredLength = Res;
			return ERROR_SUCCESS;
		}
		return ERROR_ACCESS_DENIED;
	};

	static const DWORD PathDataSize = 512;
	wchar_t			   PathData[PathDataSize];
	DWORD			   RequiredLengthIncludingNul = 0;
	DWORD Error = GetFinalPathNameByHandleWRetry(NativeHandle, PathData, PathDataSize, FILE_NAME_OPENED, RequiredLengthIncludingNul);
	if (Error != ERROR_SUCCESS)
	{
		Ec = MakeErrorCodeFromLastError();
		return fmt::format("<error handle '{}'>", Ec.message());
	}

	if (RequiredLengthIncludingNul < PathDataSize)
	{
		std::wstring FullPath(PathData, gsl::narrow<size_t>(RequiredLengthIncludingNul));
		return FullPath;
	}

	std::wstring FullPath;
	FullPath.resize(RequiredLengthIncludingNul - 1);

	DWORD FinalLength = 0;
	Error = GetFinalPathNameByHandleWRetry(NativeHandle, FullPath.data(), RequiredLengthIncludingNul, FILE_NAME_OPENED, FinalLength);
	if (Error != ERROR_SUCCESS)
	{
		Ec = MakeErrorCodeFromLastError();
		return fmt::format("<error handle '{}'>", Ec.message());
	}
	ZEN_UNUSED(FinalLength);
	return FullPath;

#elif ZEN_PLATFORM_LINUX
	char Link[PATH_MAX];
	char Path[64];

	sprintf(Path, "/proc/self/fd/%d", int(uintptr_t(NativeHandle)));
	ssize_t BytesRead = readlink(Path, Link, sizeof(Link) - 1);
	if (BytesRead <= 0)
	{
		Ec = MakeErrorCodeFromLastError();
		return fmt::format("<error handle '{}'>", Ec.message());
	}

	Link[BytesRead] = '\0';
	return Link;
#elif ZEN_PLATFORM_MAC
	int	 Fd = int(uintptr_t(NativeHandle));
	char Path[MAXPATHLEN];
	if (fcntl(Fd, F_GETPATH, Path) < 0)
	{
		Ec = MakeErrorCodeFromLastError();
		return fmt::format("<error handle '{}'>", Ec.message());
	}

	return Path;
#endif	// ZEN_PLATFORM_WINDOWS
}

std::filesystem::path
PathFromHandle(void* NativeHandle)
{
	std::error_code		  Ec;
	std::filesystem::path Result = PathFromHandle(NativeHandle, Ec);
	if (Ec)
	{
		throw std::system_error(Ec, fmt::format("failed to get path from file handle '{}'", NativeHandle));
	}
	return Result;
}

uint64_t
FileSizeFromHandle(void* NativeHandle)
{
	uint64_t FileSize = ~0ull;

#if ZEN_PLATFORM_WINDOWS
	BY_HANDLE_FILE_INFORMATION Bhfh = {};
	if (GetFileInformationByHandle(NativeHandle, &Bhfh))
	{
		FileSize = uint64_t(Bhfh.nFileSizeHigh) << 32 | Bhfh.nFileSizeLow;
	}
#else
	int			Fd = int(intptr_t(NativeHandle));
	struct stat Stat;
	fstat(Fd, &Stat);
	FileSize = size_t(Stat.st_size);
#endif

	return FileSize;
}

std::filesystem::path
GetRunningExecutablePath()
{
#if ZEN_PLATFORM_WINDOWS
	// TODO: make this long path aware

	TCHAR ExePath[MAX_PATH];
	DWORD PathLength = GetModuleFileName(NULL, ExePath, ZEN_ARRAY_COUNT(ExePath));

	return {std::wstring_view(ExePath, PathLength)};
#elif ZEN_PLATFORM_LINUX
	char	Link[256];
	ssize_t BytesRead = readlink("/proc/self/exe", Link, sizeof(Link) - 1);
	if (BytesRead < 0)
		return {};

	Link[BytesRead] = '\0';
	return Link;
#elif ZEN_PLATFORM_MAC
	char Buffer[PROC_PIDPATHINFO_MAXSIZE];

	int SelfPid = GetCurrentProcessId();
	if (proc_pidpath(SelfPid, Buffer, sizeof(Buffer)) <= 0)
		return {};

	return Buffer;
#endif	// ZEN_PLATFORM_WINDOWS
}

void
MaximizeOpenFileCount()
{
#if ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC
	struct rlimit Limit;
	int			  Error = getrlimit(RLIMIT_NOFILE, &Limit);
	if (Error)
	{
		ZEN_WARN("failed getting rlimit RLIMIT_NOFILE, reason '{}'", zen::MakeErrorCode(Error).message());
	}
	else
	{
		struct rlimit NewLimit = Limit;
		NewLimit.rlim_cur	   = NewLimit.rlim_max;
		ZEN_DEBUG("changing RLIMIT_NOFILE from rlim_cur = {}, rlim_max {} to rlim_cur = {}, rlim_max {}",
				  Limit.rlim_cur,
				  Limit.rlim_max,
				  NewLimit.rlim_cur,
				  NewLimit.rlim_max);

		Error = setrlimit(RLIMIT_NOFILE, &NewLimit);
		if (Error != 0)
		{
			ZEN_WARN("failed to set RLIMIT_NOFILE limits from rlim_cur = {}, rlim_max {} to rlim_cur = {}, rlim_max {}, reason '{}'",
					 Limit.rlim_cur,
					 Limit.rlim_max,
					 NewLimit.rlim_cur,
					 NewLimit.rlim_max,
					 zen::MakeErrorCode(Error).message());
		}
	}
#endif
}

void
GetDirectoryContent(const std::filesystem::path& RootDir, uint8_t Flags, DirectoryContent& OutContent)
{
	FileSystemTraversal Traversal;
	struct Visitor : public FileSystemTraversal::TreeVisitor
	{
		Visitor(uint8_t Flags, DirectoryContent& OutContent) : Flags(Flags), Content(OutContent) {}

		virtual void VisitFile([[maybe_unused]] const std::filesystem::path& Parent,
							   [[maybe_unused]] const path_view&			 File,
							   [[maybe_unused]] uint64_t					 FileSize) override
		{
			if (Flags & DirectoryContent::IncludeFilesFlag)
			{
				Content.Files.push_back(Parent / File);
			}
		}

		virtual bool VisitDirectory([[maybe_unused]] const std::filesystem::path& Parent, const path_view& DirectoryName) override
		{
			if (Flags & DirectoryContent::IncludeDirsFlag)
			{
				Content.Directories.push_back(Parent / DirectoryName);
			}
			return (Flags & DirectoryContent::RecursiveFlag) != 0;
		}

		const uint8_t	  Flags;
		DirectoryContent& Content;
	} Visit(Flags, OutContent);

	Traversal.TraverseFileSystem(RootDir, Visit);
}

std::string
GetEnvVariable(std::string_view VariableName)
{
	ZEN_ASSERT(!VariableName.empty());
#if ZEN_PLATFORM_WINDOWS

	std::vector<CHAR> EnvVariableBuffer(1023 + 1);
	DWORD RESULT = GetEnvironmentVariableA(std::string(VariableName).c_str(), EnvVariableBuffer.data(), (DWORD)EnvVariableBuffer.size());
	if (RESULT == 0)
	{
		return "";
	}
	if (RESULT <= EnvVariableBuffer.size())
	{
		return std::string(EnvVariableBuffer.data(), size_t(RESULT));
	}
	EnvVariableBuffer.resize(size_t(RESULT));
	RESULT = GetEnvironmentVariableA(std::string(VariableName).c_str(), EnvVariableBuffer.data(), (DWORD)EnvVariableBuffer.size());
	if (RESULT == 0)
	{
		return "";
	}
	if (RESULT <= EnvVariableBuffer.size())
	{
		return std::string(EnvVariableBuffer.data(), size_t(RESULT));
	}
#endif
#if ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC
	char* EnvVariable = getenv(std::string(VariableName).c_str());
	if (EnvVariable)
	{
		return std::string(EnvVariable);
	}
#endif
	return "";
}

std::error_code
RotateFiles(const std::filesystem::path& Filename, std::size_t MaxFiles)
{
	const std::filesystem::path BasePath(Filename.parent_path());
	const std::string			Stem(Filename.stem().string());
	const std::string			Extension(Filename.extension().string());

	std::error_code Result;

	auto GetFileName = [&](size_t Index) -> std::filesystem::path {
		if (Index == 0)
		{
			return BasePath / (Stem + Extension);
		}
		return BasePath / fmt::format("{}.{}{}", Stem, Index, Extension);
	};

	auto IsEmpty = [](const std::filesystem::path& Path, std::error_code& Ec) -> bool {
		bool Exists = std::filesystem::exists(Path, Ec);
		if (Ec)
		{
			return false;
		}
		if (!Exists)
		{
			return true;
		}
		uintmax_t Size = std::filesystem::file_size(Path, Ec);
		if (Ec)
		{
			return false;
		}
		return Size == 0;
	};

	bool BaseIsEmpty = IsEmpty(GetFileName(0), Result);
	if (Result)
	{
		return Result;
	}
	if (!BaseIsEmpty)
	{
		// We try our best to rotate the logs, if we fail we fail and will try to open the base log file anyway
		for (auto i = MaxFiles; i > 0; i--)
		{
			std::filesystem::path src = GetFileName(i - 1);
			if (!std::filesystem::exists(src))
			{
				continue;
			}
			std::error_code		  DummyEc;
			std::filesystem::path target = GetFileName(i);
			if (std::filesystem::exists(target, DummyEc))
			{
				std::filesystem::remove(target, DummyEc);
			}
			std::filesystem::rename(src, target, DummyEc);
		}
	}

	return Result;
}

std::error_code
RotateDirectories(const std::filesystem::path& DirectoryName, std::size_t MaxDirectories)
{
	const std::filesystem::path BasePath(DirectoryName.parent_path());
	const std::string			Stem(DirectoryName.stem().string());

	auto GetPathForIndex = [&](size_t Index) -> std::filesystem::path {
		if (Index == 0)
		{
			return BasePath / Stem;
		}
		return BasePath / fmt::format("{}.{}", Stem, Index);
	};

	auto IsEmpty = [](const std::filesystem::path& Path, std::error_code& Ec) -> bool { return std::filesystem::is_empty(Path, Ec); };

	std::error_code Result;
	const bool		BaseIsEmpty = IsEmpty(GetPathForIndex(0), Result);
	if (Result)
	{
		return Result;
	}

	if (BaseIsEmpty)
		return Result;

	for (std::size_t i = MaxDirectories; i > 0; i--)
	{
		const std::filesystem::path SourcePath = GetPathForIndex(i - 1);

		if (std::filesystem::exists(SourcePath))
		{
			std::filesystem::path TargetPath = GetPathForIndex(i);

			std::error_code DummyEc;
			if (std::filesystem::exists(TargetPath, DummyEc))
			{
				std::filesystem::remove_all(TargetPath, DummyEc);
			}
			std::filesystem::rename(SourcePath, TargetPath, DummyEc);
		}
	}

	return Result;
}

std::filesystem::path
SearchPathForExecutable(std::string_view ExecutableName)
{
#if ZEN_PLATFORM_WINDOWS
	std::wstring Executable(Utf8ToWide(ExecutableName));

	DWORD Result = SearchPathW(nullptr, Executable.c_str(), L".exe", 0, nullptr, nullptr);

	if (!Result)
		return ExecutableName;

	auto PathBuffer = std::make_unique_for_overwrite<WCHAR[]>(Result);

	Result = SearchPathW(nullptr, Executable.c_str(), L".exe", Result, PathBuffer.get(), nullptr);

	if (!Result)
		return ExecutableName;

	return PathBuffer.get();
#else
	return ExecutableName;
#endif
}

//////////////////////////////////////////////////////////////////////////
//
// Testing related code follows...
//

#if ZEN_WITH_TESTS

void
filesystem_forcelink()
{
}

TEST_CASE("filesystem")
{
	using namespace std::filesystem;

	// GetExePath -- this is not a great test as it's so dependent on where the this code gets linked in
	path	   BinPath	   = GetRunningExecutablePath();
	const bool ExpectedExe = PathToUtf8(BinPath.stem().native()).ends_with("-test"sv) || BinPath.stem() == "zenserver";
	CHECK(ExpectedExe);
	CHECK(is_regular_file(BinPath));

	// PathFromHandle
	void* Handle;
#	if ZEN_PLATFORM_WINDOWS
	Handle = CreateFileW(BinPath.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, nullptr);
	CHECK(Handle != INVALID_HANDLE_VALUE);
#	else
	int Fd = open(BinPath.c_str(), O_RDONLY | O_CLOEXEC);
	CHECK(Fd >= 0);
	Handle = (void*)uintptr_t(Fd);
#	endif

	auto FromHandle = PathFromHandle((void*)uintptr_t(Handle));
	CHECK(equivalent(FromHandle, BinPath));

#	if ZEN_PLATFORM_WINDOWS
	CloseHandle(Handle);
#	else
	close(int(uintptr_t(Handle)));
#	endif

	// Traversal
	struct : public FileSystemTraversal::TreeVisitor
	{
		virtual void VisitFile(const std::filesystem::path& Parent, const path_view& File, uint64_t) override
		{
			bFoundExpected |= std::filesystem::equivalent(Parent / File, Expected);
		}

		virtual bool VisitDirectory(const std::filesystem::path&, const path_view&) override { return true; }

		bool				  bFoundExpected = false;
		std::filesystem::path Expected;
	} Visitor;
	Visitor.Expected = BinPath;

	FileSystemTraversal().TraverseFileSystem(BinPath.parent_path().parent_path(), Visitor);
	CHECK(Visitor.bFoundExpected);

	// Scan/read file
	FileContents		 BinRead = ReadFile(BinPath);
	std::vector<uint8_t> BinScan;
	ScanFile(BinPath, 16 << 10, [&](const void* Data, size_t Size) {
		const auto* Ptr = (uint8_t*)Data;
		BinScan.insert(BinScan.end(), Ptr, Ptr + Size);
	});
	CHECK_EQ(BinRead.Data.size(), 1);
	CHECK_EQ(BinScan.size(), BinRead.Data[0].GetSize());
}

TEST_CASE("WriteFile")
{
	std::filesystem::path TempFile = GetRunningExecutablePath().parent_path();
	TempFile /= "write_file_test";

	uint64_t Magics[] = {
		0x0'a9e'a9e'a9e'a9e'a9e,
		0x0'493'493'493'493'493,
	};

	struct
	{
		const void* Data;
		size_t		Size;
	} MagicTests[] = {
		{
			Magics,
			sizeof(Magics),
		},
		{
			Magics + 1,
			sizeof(Magics[0]),
		},
	};
	for (auto& MagicTest : MagicTests)
	{
		WriteFile(TempFile, IoBuffer(IoBuffer::Wrap, MagicTest.Data, MagicTest.Size));

		FileContents MagicsReadback = ReadFile(TempFile);
		CHECK_EQ(MagicsReadback.Data.size(), 1);
		CHECK_EQ(MagicsReadback.Data[0].GetSize(), MagicTest.Size);
		CHECK_EQ(memcmp(MagicTest.Data, MagicsReadback.Data[0].Data(), MagicTest.Size), 0);
	}

	std::filesystem::remove(TempFile);
}

TEST_CASE("DiskSpaceInfo")
{
	std::filesystem::path BinPath = GetRunningExecutablePath();

	DiskSpace Space = {};

	std::error_code Error;
	Space = DiskSpaceInfo(BinPath, Error);
	CHECK(!Error);

	bool Okay = DiskSpaceInfo(BinPath, Space);
	CHECK(Okay);

	CHECK(int64_t(Space.Total) > 0);
	CHECK(int64_t(Space.Free) > 0);	 // Hopefully there's at least one byte free
}

TEST_CASE("PathBuilder")
{
#	if ZEN_PLATFORM_WINDOWS
	const char* foo_bar = "/foo\\bar";
#	else
	const char* foo_bar = "/foo/bar";
#	endif

	ExtendablePathBuilder<32> Path;
	for (const char* Prefix : {"/foo", "/foo/"})
	{
		Path.Reset();
		Path.Append(Prefix);
		Path /= "bar";
		CHECK(Path.ToPath() == foo_bar);
	}

	using fspath = std::filesystem::path;

	Path.Reset();
	Path.Append(fspath("/foo/"));
	Path /= (fspath("bar"));
	CHECK(Path.ToPath() == foo_bar);

#	if ZEN_PLATFORM_WINDOWS
	Path.Reset();
	Path.Append(fspath(L"/\u0119oo/"));
	Path /= L"bar";
	printf("%ls\n", Path.ToPath().c_str());
	CHECK(Path.ToView() == L"/\u0119oo/bar");
	CHECK(Path.ToPath() == L"\\\u0119oo\\bar");
#	endif
}

TEST_CASE("RotateDirectories")
{
	std::filesystem::path TestBaseDir = GetRunningExecutablePath().parent_path() / ".test";
	CleanDirectory(TestBaseDir);
	std::filesystem::path RotateDir		= TestBaseDir / "rotate_dir" / "dir_to_rotate";
	IoBuffer			  DummyFileData = IoBufferBuilder::MakeCloneFromMemory("blubb", 5);

	auto NewDir = [&] {
		CreateDirectories(RotateDir);
		WriteFile(RotateDir / ".placeholder", DummyFileData);
	};

	auto DirWithSuffix = [&](int Index) -> std::filesystem::path { return RotateDir.generic_string().append(fmt::format(".{}", Index)); };

	const int RotateMax = 10;

	NewDir();
	CHECK(std::filesystem::exists(RotateDir));
	RotateDirectories(RotateDir, RotateMax);
	CHECK(!std::filesystem::exists(RotateDir));
	CHECK(std::filesystem::exists(DirWithSuffix(1)));
	NewDir();
	CHECK(std::filesystem::exists(RotateDir));
	RotateDirectories(RotateDir, RotateMax);
	CHECK(!std::filesystem::exists(RotateDir));
	CHECK(std::filesystem::exists(DirWithSuffix(1)));
	CHECK(std::filesystem::exists(DirWithSuffix(2)));

	for (int i = 0; i < RotateMax; ++i)
	{
		NewDir();
		std::error_code Ec		= RotateDirectories(RotateDir, 10);
		const bool		IsError = !!Ec;
		CHECK_EQ(IsError, false);
	}

	CHECK(!std::filesystem::exists(RotateDir));

	for (int i = 0; i < RotateMax; ++i)
	{
		CHECK(std::filesystem::exists(DirWithSuffix(i + 1)));
	}

	for (int i = RotateMax; i < RotateMax + 5; ++i)
	{
		CHECK(!std::filesystem::exists(DirWithSuffix(RotateMax + i + 1)));
	}
}

#endif

}  // namespace zen
